import parse from 'co-body';

import Context from '../context';
import { requestTypeis } from '../utils/request-type';
import { payloadTooLargeError } from '../utils/error';
import { Request } from '../request';
import { RequestBody } from '../exchange';
import { Filter, FilterChain } from '../utils/filter';

const DEFAULT_JSON_TYPES = [
  'application/json',
  'application/json-patch+json',
  'application/vnd.api+json',
  'application/csp-report',
];

const DEFAULT_FORM_TYPES = ['application/x-www-form-urlencoded'];

const DEFAULT_TEXT_TYPES = ['text/plain'];

type Types = 'json' | 'text' | 'form';

export interface BodyParserOptions {
  returnRawBody?: boolean;
  jsonLimit?: string | number;
  formLimit?: string | number;
  textLimit?: string | number;
  extendTypes?: { [key in Types]?: string[] };
}

function getExtendTypes(opts: BodyParserOptions, type: Types): string[] {
  if (opts === null || opts === undefined) return [];
  const { extendTypes } = opts;
  if (extendTypes === null || extendTypes === undefined) return [];
  switch (type) {
    case 'form':
      return extendTypes.form || [];
    case 'json':
      return extendTypes.json || [];
    case 'text':
      return extendTypes.text || [];
    default:
      return [];
  }
}

export default (opts?: BodyParserOptions): Filter<Context> => {
  const DEFAULT_OPTS: BodyParserOptions = {
    returnRawBody: true,
    jsonLimit: '1mb',
    formLimit: '56kb',
    textLimit: 512,
  };

  opts = Object.assign(DEFAULT_OPTS, opts);

  const jsonTypes = [...DEFAULT_JSON_TYPES, ...getExtendTypes(opts, 'json')];
  const formTypes = [...DEFAULT_FORM_TYPES, ...getExtendTypes(opts, 'form')];
  const textTypes = [...DEFAULT_TEXT_TYPES, ...getExtendTypes(opts, 'text')];

  // force co-body return raw body
  const jsonOpts = { returnRawBody: opts.returnRawBody, limit: opts.jsonLimit };
  const formOpts = { returnRawBody: opts.returnRawBody, limit: opts.formLimit };
  const textOpts = { returnRawBody: opts.returnRawBody, limit: opts.textLimit };

  return async (ctx: Context, chain: FilterChain<Context>): Promise<void> => {
    if (ctx.request.body !== undefined) {
      return await chain.next(ctx);
    }

    const result = await parseBody(ctx.request);
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    if (opts!.returnRawBody) {
      const withRaw = result as RequestBodyWithRaw;
      ctx.request.body = 'parsed' in withRaw ? withRaw.parsed : {};
      ctx.request.setAttr('rawBody', withRaw.raw || null);
    } else {
      ctx.request.body = result;
    }
    await chain.next(ctx);
  };

  interface RequestBodyWithRaw {
    parsed?: RequestBody;
    raw?: string;
  }

  async function parseBody(request: Request): Promise<RequestBodyWithRaw | RequestBody> {
    const contentType = request.headers['content-type'] || '';
    try {
      if (requestTypeis(contentType, jsonTypes)) {
        return await parse.json(request.req, jsonOpts);
      }
      if (requestTypeis(contentType, formTypes)) {
        return await parse.form(request.req, formOpts);
      }
      if (requestTypeis(contentType, textTypes)) {
        return (await parse.text(request.req, textOpts)) || '';
      }
    } catch (err) {
      if (err.type === 'entity.too.large') {
        console.log(JSON.stringify(err, null, '    ')); // FIXME: 调整这里的日志
        throw payloadTooLargeError();
      }
      throw err;
    }
    return {};
  }
};
